/**
* @file pci.c
* Conventional PCI driver - implementation.
* Basic handler for emuration of device connected to PCI.
*
* To make first version of this driver I have gotten some inspiration
* from "Minirighi OS" drivers. Thanks!
*
* Copyrights 2011 Michal Saman, m.saman@designplus.cz.
* This source code is release under the Apache License 2.0.m
* http://www.apache.org/licenses/LICENSE-2.0
*/

#include <env.h>
#include <drivers/pci/pci.h>
#include <drivers/pci/pci_db.h>
#include <hal/hal.h>
#include <main/kprintf.h>

PUBLIC struct pci	_pci;

/**
 * Read register from PCI Configuration Space.
 * Space is addressable by bus number, device number, function number, register offset
 * and register size.
 * @param	bus			Bus id
 * @param	device		Device id
 * @param	function	Function id
 * @param	reg_offset	Offset inside configuration register (in bytes)
 * @param	count		Count of bytes to read (possible values are: 8, 16, 32)
 */
uint32_t pci_cfg_read(unsigned bus, unsigned device, unsigned function, unsigned reg_offset, size_t count) {
	pci_register_address_t pci_addr;

	pci_addr.bit_mask = 0;
	pci_addr.data.bus = bus;
	pci_addr.data.device = device;
	pci_addr.data.function = function;
	pci_addr.data.reg = reg_offset & 0xFC;			// limitation to max 100000 = 64bytes (header of configuration space).
	pci_addr.data.rsvd = 0;
	pci_addr.data.enable = 1;

	//kprintf("READ Conf address: %x\n", pci_addr.bit_mask);
	outportdw(PCI_CONF_ADDRESS, pci_addr.bit_mask);
	switch (count) {
		case 32 :
			return inportdw(PCI_CONF_DATA);
		case 16 :
			return inportw(PCI_CONF_DATA);
		default :
			return inportb(PCI_CONF_DATA);
	}
}

void pci_cfg_write(unsigned bus, unsigned device, unsigned function, unsigned reg_offset, uint32_t value, size_t count) {
	pci_register_address_t pci_addr;

	pci_addr.bit_mask = 0;
	pci_addr.data.bus = bus;
	pci_addr.data.device = device;
	pci_addr.data.function = function;
	pci_addr.data.reg = reg_offset & 0xFC;			// limitation to max 100000 = 64bytes (header of configuration space).
	pci_addr.data.rsvd = 0;
	pci_addr.data.enable = 1;

	outportdw(PCI_CONF_ADDRESS, pci_addr.bit_mask);
	switch (count) {
		case 32 :
			outportdw(PCI_CONF_DATA, value);
			break;
		case 16 :
			outportw(PCI_CONF_DATA, (uint16_t)value);
			break;
		default :
			outportb(PCI_CONF_DATA, (uint8_t)value);
	}
}

/**
 * Function calculate size of region defined by base register.
 * The calculation is based on fact that if we write all 1's to base register
 * the PCI bus return aligned region.
 * @param base_reg_value	Test value obtained from base reg. after write.
 * @param addr_mask			Mask of address in base register
 * @return Size of memory region or maximum offset of port address (in case of IO port)
 */
uint32_t pci_region_size(uint32_t base_reg_value, unsigned long addr_mask) {
	uint32_t s = addr_mask & base_reg_value;
	s = s & ~(s-1);
	return (s-1);
}

/**
 * Read base address registers form PCI configuration space.
 * Normal PCI device has 6 base registers starting at 0x10.
 * PCI-PCI bridge has 2 base regs and PCI cardbus has 1.
 * @param pci_conf_space	pointer to configuration space of bus/dev/func
 * @param bases_count		number of base regs to read
 * @param rom_address		position of rom_base register inside PCI conf. space.
 */
void pci_read_bases(pci_conf_space_t * pci_conf_space, unsigned bases_cout, unsigned rom_address) {
	unsigned i;
	uint32_t reg_addr, br_val, test_val;

	// clear base regs and rom base addr
	memset(pci_conf_space->base, 0, sizeof(pci_conf_space->base));
	memset(pci_conf_space->size, 0, sizeof(pci_conf_space->size));
	memset(pci_conf_space->type, 0, sizeof(pci_conf_space->type));

	// read information from PCI conf. space
	for (i=0; i<bases_cout; i++) {
		reg_addr = PCI_FIRST_BASE_REGISTER_ADDR + (i<<2);		// calculate base register address (++ 4bytes)
		//kprintf("0x%x ", reg_addr);
		br_val = pci_cfg_read(pci_conf_space->bus, pci_conf_space->device, pci_conf_space->function, reg_addr, 32); 	// read base reg value
		//kprintf("0x%x ", br_val);
		pci_cfg_write(pci_conf_space->bus, pci_conf_space->device, pci_conf_space->function, reg_addr, ~0, 32);			// write test value (jednotkovy doplnek)
		test_val = pci_cfg_read(pci_conf_space->bus, pci_conf_space->device, pci_conf_space->function, reg_addr, 32); 	// read base reg value that we wrote
		pci_cfg_write(pci_conf_space->bus, pci_conf_space->device, pci_conf_space->function, reg_addr, br_val, 32);		// retur previous value to register

		// test if we can successfully write to register
		// test_val must be true or different from FF
		if (!test_val || test_val == 0xFFFFFFFF)
			continue;			// can't read base register, continue to next
		if (br_val == 0xFFFFFFFF)		//??
			br_val = 0;

		// determine type of base register
		// and store information about region
		if ((br_val & PCI_BASE_REGISTER_LOWEST_BIT_MASK) == PCI_BASE_REGISTER_IS_MEMORY) {
			pci_conf_space->base[i] = br_val & PCI_BASE_REGISTER_MEM_ADDR_MASK;
			pci_conf_space->type[i] = PCI_BASE_REGISTER_IS_MEMORY;
			pci_conf_space->size[i] = pci_region_size(test_val, PCI_BASE_REGISTER_MEM_ADDR_MASK);
		} else { // I/O based
			pci_conf_space->base[i] = br_val & PCI_BASE_REGISTER_IO_ADDR_MASK;
			pci_conf_space->type[i] = PCI_BASE_REGISTER_IS_IO;
			pci_conf_space->size[i] = pci_region_size(test_val, PCI_BASE_REGISTER_IO_ADDR_MASK);
		}

		// ROM register
		if (rom_address) {
			// init/reset values
			pci_conf_space->rom_base = 0;
			pci_conf_space->rom_size = 0;

			br_val = pci_cfg_read(pci_conf_space->bus, pci_conf_space->device, pci_conf_space->function, rom_address, 32); 	// read rom reg. value
			pci_cfg_write(pci_conf_space->bus, pci_conf_space->device, pci_conf_space->function, rom_address, ~1, 32);		// write test value (jednotkovy doplnek)
			test_val = pci_cfg_read(pci_conf_space->bus, pci_conf_space->device, pci_conf_space->function, rom_address, 32); 	// read base reg value that we wrote
			pci_cfg_write(pci_conf_space->bus, pci_conf_space->device, pci_conf_space->function, rom_address, br_val, 32);		// retur previous value to register

			if (br_val == 0xFFFFFFFF)
				br_val = 0;
			if (test_val && br_val != 0xFFFFFFFF) {
				pci_conf_space->rom_base = br_val & PCI_ROM_ADDR_MASK;
				pci_conf_space->rom_size = pci_region_size(test_val, PCI_ROM_ADDR_MASK);
			}
		}
	}
}

/**
 * Probe PCI bus and result returns as pointer to PCI Configuration Space.
 * @param	bus					Bus id
 * @param	device				Device id
 * @param	function			Function id
 * @param	pci_conf_space_t	Pointer to filled PCI configuration space structure as result
 * @return	TRUE if device is present, FALSE if device is not present
 */
bool pci_probe(unsigned bus, unsigned device, unsigned function, pci_conf_space_t * pci_conf_space) {

	// assign address of conf space struct to temporary DW pointer, so when we write tmp pointer
	// we write to conf structure too.
	uint32_t * dw_tmp = (uint32_t *) pci_conf_space;

	//kprintf("Probing PCI bus %i, device %i, function %i ...\n", bus, device, function);
	unsigned i;
	// read four DW from conf space (header)
	for (i=0; i<4; i++) {
		dw_tmp[i] = pci_cfg_read(bus, device, function, (i << 2), 32);
	}

	//kprintf("VendorID: %x\n", pci_conf_space->vendor_id);

	// check if vendor id is set (if device/function not exist, the bus master set header to FFFFFFFF
	if (pci_conf_space->vendor_id == 0xFFFF) {
		//kprintf("Probing PCI bus %i, device %i, function %i ...\n", bus, device, function);
		return false;
	}

	pci_conf_space->bus = bus;
	pci_conf_space->device = device;
	pci_conf_space->function = function;

	switch (pci_conf_space->header_type & pci_header_type_mask) {
		case PCI_HEADER_TYPE_NORMAL :
			pci_read_bases(pci_conf_space, pci_conf_space_base_address_count, PCI_ROM_BASE_REGISTER_ADDR);
			break;
		case PCI_HEADER_TYPE_BRIDGE :
			break;
		case PCI_HEADER_TYPE_CARDBUS :
			break;
	}

	return true;
}

/**
 * Emuration of all PCI devices in system.
 * Tries to probe alll pci buses and all devices.
 */
void pci_emuration() {
	kprintf(">>> Scannig PCI bus... ");
	unsigned bus, device, function, i;
	i = 0;
	pci_conf_space_t pci_conf;
	pci_conf_space_t* new_confs;

	for (bus=0; bus < pci_bus_max_count; bus++)
	 for (device=0; device < pci_device_max_count; device++)
	  for (function=0; function < pci_device_functions_max_count; function++) {
		  if (pci_probe(bus, device, function, &pci_conf)) {
			  new_confs = (pci_conf_space_t *) kmalloc((i+1)*sizeof(pci_conf_space_t));		// resize array and save pointer to temporary variable
			  if (i>0) {
				  memcpy(new_confs, _pci.pci_confs, i*sizeof(pci_conf_space_t));			// copy old array to new larger array
				  kfree(_pci.pci_confs);													// free old array !! TODO KHEAP free is unstable and buggy, if enable some data leaks...
			  }
			  _pci.pci_confs = new_confs;												// change pointer to valid array
			  new_confs = NULL;															// discart temporary variable
			  memcpy(&_pci.pci_confs[i++], &pci_conf, sizeof(pci_conf_space_t));		// copy new device conf_space
		  }
	}
	_pci.count = i;
	kprintf("DONE. Found %i PCI devices.\n", i);
}


void pci_init() {
	_pci.count = 0;
	//_pci.pci_confs = (pci_conf_space_t *) kmalloc(sizeof(pci_conf_space_t));
	//kprintf("Variable _pci.pci_confs at 0x%x contains 0x%x\n", _pci.pci_confs, _pci.pci_confs[0]);

	pci_emuration();
}

void pci_show_info() {
	unsigned i;
	pci_conf_space_t * tmp ;
	for (i=0; i < _pci.count; i++) {
		tmp = &_pci.pci_confs[i];
		switch (tmp->header_type & pci_header_type_mask) {
			case PCI_HEADER_TYPE_NORMAL :
				kprintf("pci device=> vid: %x (%s), did: %x, class: %x-%x-%x\n",
						tmp->vendor_id,
						pci_get_vendor(tmp->vendor_id).VenFull,
						tmp->device_id,
						tmp->base_class,
						tmp->sub_class,
						tmp->interface_code);
				//kprintf("  -> @(b: %i, d: %i, f: %i)\n", pci_conf_space->bus, pci_conf_space->device, pci_conf_space->function);
				/**
				for (i=0; i< pci_conf_space_base_address_count; i++)
					if (pci_conf_space->base[i])
						kprintf("  -> base region [%i] start: 0x%x, size: 0x%x, type: %i\n", i, pci_conf_space->base[i], pci_conf_space->size[i], pci_conf_space->type[i]);
				*/
				break;
			case PCI_HEADER_TYPE_BRIDGE :
				/**
				kprintf("pci bridge=> vid: %x (%s), did: %x, class: %x-%x-%x\n",
						tmp->vendor_id,
						pci_get_vendor(tmp->vendor_id).VenFull,
						tmp->device_id,
						tmp->base_class,
						tmp->sub_class,
						tmp->interface_code);
				kprintf("  -> @(b: %i, d: %i, f: %i)\n", tmp->bus, tmp->device, tmp->function);
				*/
				break;
			case PCI_HEADER_TYPE_CARDBUS :
				kprintf("pci cardbus, vendor id: %x\n", tmp->vendor_id);
				break;
		}
	}
}

/**
 * Tries to search vendor id database and fill vendor information structure.
 * @param vendor_id	vendor id
 * @return filled PCI_VENTABLE structure
 */
PCI_VENTABLE pci_get_vendor(unsigned vendor_id) {
	int i;
	for (i=0; i<PCI_VENTABLE_LEN; i++) {
		if (PciVenTable[i].VenId == vendor_id) {
			return PciVenTable[i];
		}
	}
	return PciVenTable[0];
}
